#!/usr/bin/python2.5
# -*- coding: utf-8 -*-

"""
 JooScript parser just extends JavaScript with some new constructions.
 
 Adds:

  * package
  * class
  * let replaces var, that is keyword var is deprecated
  * virtual namespaces
  * null is deprecated, undefined is deprecated, instead null object is "none"

 Created Jan 2011 by Evgen Burzak <buzzilo@gmail.com>
"""

from pyjon import jsparser, lexer
import defs

Node = jsparser.Node

defs.map(globals())

class Tokenizer(lexer.Tokenizer):
    """
    Slightly hacked JS syntax ...
    """
    def __init__(self, *args):
        lexer.Tokenizer.__init__(self, *args)
        self.keywords = defs.keywords
        self.opTokens = self.opTokens.copy()
        self.opTokens["="][">"] = {"op":"=>"}
        self.tokenIds = defs.tokenIds
        self.opTypeNames = defs.opTypeNames

    def lexIdent(self, ch) :
        lexer.Tokenizer.lexIdent(self, ch)
        value = self.token.value
        if value in ("and", "or", "not"):
            self.token.type_ = defs.tokenIds.get(value.upper())
        elif value == "is":
            self.token.type_ = STRICT_EQ

    def lexOp(self, ch) :
        lexer.Tokenizer.lexOp(self, ch)
        if self.token.type_ in (AND, OR, NOT, STRICT_EQ):
            raise self.newSyntaxError(conformPythonSyntax("&&, ||, ! and ==="))

def scriptInit() :
    return { "type": SCRIPT,
             "funDecls": [],
             "varDecls": [],
             "clsDecls": [],
             "roleDecls": [],
             "modDecls": [],
             "impDecls": [],
             "expDecls": [],
             "loadDeps": [],
             "isRootNode": False,
             "hasEmptyReturn": False,
             "hasReturnWithValue": False,
             "isGenerator": False };

def Script(tz, t=None) :
    n = Node(tz, SCRIPT, **scriptInit())
    args = [tz, n, n, False, False, jsparser.NESTING_TOP]
    if t == FUNCTION:
        args[3] = True
        x = FunctionContext(*args)
    elif t == CLASS:
        x = ClassContext(*args)
    elif t == PACKAGE:
        x = PackageContext(*args)
    else:
        x = StaticContext(*args)
    x.Statements(n)
    return n

def conformPythonSyntax(msg):
    return msg + " were deprecated to conform Python syntax" # ... philosophy

class StaticContext(jsparser.StaticContext): 
    """
    Parse some JooScript code
    """
    def __init__(self, *rest, **kwargs):
        # set free mode from parens, it's cool thing!
        kwargs["parenFreeMode"] = True
        kwargs["tokens"] = defs.tokens
        kwargs["keywords"] = defs.keywords
        jsparser.StaticContext.__init__(self, *rest, **kwargs)
        self.static_context = StaticContext
        self.clsDecls = kwargs.get("clsDecls") or []
        self.roleDecls = kwargs.get("roleDecls") or []
        self.ctxVars = kwargs.get("ctxVars") or {}
        self.hash_map = kwargs.get("hash_map") or False

    # Virtual namespaces, by default all variables are public
    virtualNamespaces = [ "public", "global", "sealed", "internal", 
                          "hidden", "static", "frozen", "final" ]
    #"enumerable", "writable", "configurable" 

    def update(self, **ext):
        ext["clsDecls"] = self.clsDecls
        ext["roleDecls"] = self.roleDecls
        ext["ctxVars"] = self.ctxVars
        ctx = jsparser.StaticContext.update(self, **ext)
        return ctx

    def VirtualNamespaceDef(self, expression):
        ns = {"public":True}
        while True:
            # track END as None
            if getattr(self.tz.token, "value", None) not in self.virtualNamespaces:
                if len(ns) and self.tz.token.type_ not in (IDENTIFIER, CLASS, FUNCTION):
                    raise self.tz.newSyntaxError("Namespace must be followed by identifier or function.")
                break
            ns[self.tz.token.value] = True
            tt = self.tz.get()

        self.tz.unget()
        n = expression()
        n.namespace = ns
        return n

    def Statement(self):
        tt = self.tz.get(True)
        value = self.tz.token.value
        stmt_form = (self.nesting != jsparser.NESTING_TOP) and \
                        jsparser.STATEMENT_FORM or jsparser.DECLARED_FORM

        if value in self.virtualNamespaces:
            return self.VirtualNamespaceDef(lambda: jsparser.StaticContext.Expression(self))

        elif tt == ROLE:
            return self.RoleDefinition(jsparser.STATEMENT_FORM, True)

        elif tt == CLASS:
            return self.ClassDefinition(stmt_form)

        elif tt == PACKAGE:
            return self.PackageDefinition(stmt_form)

        elif tt == IMPORT:
            return self.ImportDefinition(stmt_form)

        elif tt in (ON, WHEN, ONCE):
            n = Node(self.tz)
            n.eventType = self.Expression()
            tt = self.tz.get()
            if tt not in (IN, ON, OVER, FROM, DO):
                self.tz.mustMatch(DO)

            if tt in (IN, ON, OVER, FROM):
                n.eventThread = self.Expression()
                self.tz.mustMatch(DO)
            
            if self.tz.match(LEFT_CURLY):
                n.eventHandler = Script(self.tz, FUNCTION)
                self.tz.mustMatch(RIGHT_CURLY)
            else:
                n.eventHandler = self.Expression()

            return n
                
        elif tt == DISPATCH:
            n = Node(self.tz)
            n.eventType = self.Expression()
            if self.tz.match(TO):
                n.eventThread = self.Expression()
            return n

        elif tt == DISMISS:
            n = Node(self.tz)
            n.eventType = self.Expression()
            if self.tz.match(FROM):
                n.eventThread = self.Expression()
            return n

        elif tt == USE:
            n = Node(self.tz)
            self.tz.mustMatch(IDENTIFIER)
            if self.tz.token.value == "capture":
                self.tz.mustMatch(COMMA) 
                n = self.Statement()
                n.useCapture = True
            elif self.tz.token.value == "strict":
                n.value = self.tz.token.value
            else:
                raise self.tz.newSyntaxError("Unknown option " + self.tz.token.value)
            return n

        self.tz.unget()
        n = jsparser.StaticContext.Statement(self)
        if n.type_ == FOR:
            raise self.tz.newSyntaxError(conformPythonSyntax("for(;;;)"))
        elif n.type_ == DO:
            raise self.tz.newSyntaxError(conformPythonSyntax("do...while"))
        return n

    # >>> let foo Int = 123
    def IdentifierDefinition__(self, *args):
        self.tz.mustMatch(IDENTIFIER)
        return self.identifierExpression()

    # >>> bar String = "abc", baz Bool = true
    # TODO make tests, I'm not sure this hack will works right in all cases ...
    def Identifier__(self, *args):
        n = Node(self.tz)
        tt = self.tz.peek()
        varType = None
        if tt in (IDENTIFIER, NONE):
            self.tz.get()
            varType = self.tz.token.value

        n.varType = varType
        return n

    #def SelfExpression(self, *args):
        #return Node(self.tz)

    # FIXME unique var id for each code block
    def Variables(self, letBlock=None):
        tt = self.tz.token.type_;
        if tt in (CONST, VAR):
            raise self.tz.newSyntaxError(conformPythonSyntax("Const and var"))
        n = jsparser.StaticContext.Variables(self, letBlock)
        return n

    def RoleDefinition(self, roleForm, requireName):
        virtualNamespaces = StaticContext.virtualNamespaces + \
                            PackageContext.virtualNamespaces + \
                            FunctionContext.virtualNamespaces + \
                            ClassContext.virtualNamespaces

        r = Node(self.tz, ROLE)
        if self.tz.match(IDENTIFIER):
            r.name = self.tz.token.value
        elif requireName:
            raise self.tz.newSyntaxError("Missing role identifier")
        self.tz.mustMatch(LEFT_CURLY)
        while True:
            spec = Node(self.tz, ROLE_ENTRY)
            spec.namespace = {}
            while True:
                tt = self.tz.get(True)
                field = self.tz.token.value
                if field == "constructor":
                    spec.constructor = True
                elif field in virtualNamespaces:
                    spec.namespace[field] = True
                elif tt == IDENTIFIER:
                    spec.name = spec.value = field
                    if self.tz.match(IDENTIFIER):
                        spec.propertyType = self.tz.token.value
                elif tt in (COMMA, RIGHT_CURLY):
                    break
                else:
                    raise self.tz.newSyntaxError("Unexpected " + defs.tokens[tt].lower())
            
            if tt == COMMA: r.append(spec)
            elif tt == RIGHT_CURLY: break
            else: raise self.tz.newSyntaxError("Invalid role syntax")
        
        r.roleForm = roleForm
        if roleForm == jsparser.DECLARED_FORM:
            self.parentScript.roleDecls.append(r)
        return r

    def PackageDefinition(self, *args):
        p = Node(self.tz, PACKAGE)
        p.group = []
        # FIXME package a.b.c. { 
        while True:
            tt = self.tz.get()
            if tt == LEFT_CURLY: break
            if tt != IDENTIFIER:
                raise self.tz.newSyntaxError("Missing package group")
            p.group.append(self.tz.token.value)
            if self.tz.peek() != LEFT_CURLY:
                self.tz.mustMatch(DOT)

        self.tz.unget()
        self.tz.mustMatch(LEFT_CURLY)
        p.body = Script(self.tz, PACKAGE)
        self.tz.mustMatch(RIGHT_CURLY)
        p.end = self.tz.token.end

        return p

    def ClassDefinition(self, classForm, requireName=True):
        c = Node(self.tz, CLASS)

        if self.tz.match(IDENTIFIER):
            c.name = self.tz.token.value
        elif requireName:
            raise self.tz.newSyntaxError("Missing class identifier")

        if self.tz.match(LEFT_PAREN):
            # parse ancestors as list
            if not self.tz.match(RIGHT_PAREN):
                c.ancestors = self.Expression()
                self.tz.mustMatch(RIGHT_PAREN)

        self.tz.mustMatch(LEFT_CURLY)
        c.body = Script(self.tz, CLASS)
        self.tz.mustMatch(RIGHT_CURLY)

        if self.tz.match(AS):
            c.role = self.Expression()

        c.end = self.tz.token.end

        c.classForm = classForm
        if classForm == jsparser.DECLARED_FORM:
            self.parentScript.clsDecls.append(c)
        return c

    # TODO for x in range([start], stop[, step]) ...
    #def ForinDefinition(self): pass
        
    #def ForDefinition(self, *args):
        #self.tz.newSyntaxError("For statement is deprecated to conform Python syntax")

    def SwitchDefinition(self, *args):
        raise self.tz.newSyntaxError(conformPythonSyntax("Switch statement"))

    def AssignExpression(self) :
        # Have to treat yield like an operand because it could be the leftmost
        # operand of the expression.
        if self.tz.match(YIELD, True):
            return self.ReturnOrYield()

        n = Node(self.tz, ASSIGN)
        lhs = self.ConditionalExpression()

        # get variable type
        if lhs.type_ == IDENTIFIER:
            if self.tz.match(IDENTIFIER):
                lhs.varType = self.tz.token.value

        if not self.tz.match(ASSIGN):
            return lhs

        if lhs.type_ in (ARRAY_INIT, OBJECT_INIT): 
            lhs.destructuredNames = self.checkDestructuring(lhs)

        # FALL THROUGH
        elif lhs.type_ in (IDENTIFIER, DOT, INDEX, HASH_SEEK, CALL): pass

        else:
            raise self.tz.newSyntaxError("Bad left-hand side of assignment")

        if n.type_ == ASSIGN:
            n.assignOp = self.tz.token.assignOp
        n.append(lhs)
        n.append(self.AssignExpression())

        return n

    def ConditionalExpression(self):
        n = self.OrExpression()
        if self.tz.match(HOOK):
            raise self.tz.newSyntaxError(conformPythonSyntax("Hook expression"))
        return n

    def UnaryExpression(self):
        tt = self.tz.peek(True)
        if tt in (INCREMENT, DECREMENT):
            raise self.tz.newSyntaxError(conformPythonSyntax("Increment and decrement"))
        else:
            return jsparser.StaticContext.UnaryExpression(self)

    def MemberExpression(self, allowCallSyntax) :
        #if self.tz.match(NEW) :
            #raise self.tz.newSyntaxError(conformPythonSyntax("New constructor"))
        n = self.PrimaryExpression()
        while True :
            tt = self.tz.get()
            if tt == END: break
            elif tt == DOT:
                n2 = Node(self.tz)
                n2.append(n)
                self.tz.mustMatch(IDENTIFIER)
                n2.append(Node(self.tz))

            elif tt == LEFT_BRACKET:
                if self.hash_map:
                    n2 = Node(self.tz, HASH_SEEK)
                else:
                    n2 = Node(self.tz, INDEX)
                n2.append(n)
                n2.append(self.Expression())
                if self.hash_map:
                    self.tz.mustMatch(VERTICAL_BAR)
                else:
                    self.tz.mustMatch(RIGHT_BRACKET)

            elif tt == LEFT_PAREN:
                if (allowCallSyntax) :
                    n2 = Node(self.tz, CALL)
                    n2.append(n)
                    n2.append(self.ArgumentList())
                else:
                    self.tz.unget()
                    return n

            # %hash = [ a => 1, [ b => 1, [ c => 1]]]
            # x = %a[ %b[ %c["foo"]]]
            #elif tt == VERTICAL_BAR:
                #n2 = Node(self.tz, HASH_SEEK)
                #n2.append(n)
                #n2.append(self.Expression())

            else:
                self.tz.unget()
                return n

            n = n2
        
        return n
        #self.tz.unget()
        #return jsparser.StaticContext.MemberExpression(self, allowCallSyntax)

    def PrimaryExpression(self):
        tt = self.tz.get(True)
        value = self.tz.token.value
        n = None

        if value == "constructor" :
            if isinstance(self, ClassContext):
                n = self.constructor = self.FunctionDefinition(True, jsparser.EXPRESSED_FORM)
            else:
                raise self.tz.newSyntaxError("Constructor not in class context")

        if tt == CLASS:
            n = self.ClassDefinition(jsparser.EXPRESSED_FORM, requireName=False)

        elif tt == ROLE:
            n = self.RoleDefinition(jsparser.EXPRESSED_FORM, False)

        elif tt == NONE:
            n = Node(self.tz, NONE)

        # treat to "from" and "over" as identifiers in expressions
        elif tt in (FROM, OVER):
            n = Node(self.tz, IDENTIFIER)

        # context objects, it should be not used anywhere
        elif tt in (SELF, UNIT, ROOT, GLOB):
            n = Node(self.tz)

        elif tt == HASHMAP_INIT:
            n = Node(self.tz, HASHMAP_INIT)
            while True:
                tt = self.tz.peek(True)
                if tt == VERTICAL_BAR: break
                elif tt == COMMA:
                    self.tz.get()
                    n.append(None)
                    continue
                #n.append(self.AssignExpression())
                n2 = Node(self.tz, HASHMAP_ENTRY)
                n2.append(self.AssignExpression())
                if self.tz.match(HASHMAP_ENTRY):
                    n2.append(self.AssignExpression())
                n.append(n2)
                if (tt != COMMA and not self.tz.match(COMMA)):
                    break
                    
            self.tz.mustMatch(VERTICAL_BAR)

        # TODO objects props etc
        #elif value in defs.virtualNamespaces:
            #self.tz.get(True)
            #return self.VirtualNamespaceDef()

        if n: 
            return n
        else:
            self.tz.unget()
            n = jsparser.StaticContext.PrimaryExpression(self)
        #if n.type_ == NULL or (n.type_ == IDENTIFIER and n.value == "undefined"):
            #raise self.tz.newSyntaxError("Null object is none")
        return n

    def ImportDefinition(self, stmtForm):
        n = Node(self.tz, IMPORT)
        n.groups = []
        group = []
        while True:
            tt = self.tz.get()
            if tt != IDENTIFIER:
                if tt == DOT and len(group) == 0:
                    raise self.tz.newSyntaxError("Missing group to import")
            if tt == SEMICOLON: break
            elif tt in (IDENTIFIER, MUL):
                group.append(self.tz.token.value)
            elif tt == DOT:
                pass
            elif tt == COMMA:
                n.groups.append(group)
                group = []

        if len(group):
            n.groups.append(group)

        return n

class FunctionContext(StaticContext):
    """
    Function context
    """
    def __init__(self, parent, *args, **rest):
        StaticContext.__init__(self, parent, *args, **rest)


class PackageContext(StaticContext):
    """
    Package context
    """
    def __init__(self, *args, **rest):
        StaticContext.__init__(self, *args, **rest)
        #self.parentContext = parent


class ClassContext(StaticContext):
    """
    Class context
    """
    def __init__(self, *args, **rest):
        StaticContext.__init__(self, *args, **rest)
        #self.parentContext = parent
        self.constructor = None

    # Virtual namespaces
    virtualNamespaces = [] + StaticContext.virtualNamespaces 

    def AssignExpression(self) :
        tt = self.tz.peek()
        # expression closure
        if self.tz.token.type_ == ASSIGN and tt == FUNCTION:
            self.tz.get()
            f = self.FunctionDefinition(False, jsparser.EXPRESSED_FORM, False)
            return f
        else:
            return StaticContext.AssignExpression(self)

    def FunctionDefinition(self, requireName, functionForm, exprForm=True) :
        f = Node(self.tz, FUNCTION, params=[], paramsType=[], defaultParams=[])
        # TODO getter and setter
        #if (f.type_ != FUNCTION):
            #if f.value == "get": f.type_ = GETTER 
            #else: f.type_ = SETTER

        if exprForm:
            if self.tz.match(IDENTIFIER):
                f.name = self.tz.token.value
                if self.tz.match(IDENTIFIER):
                    f.returnType = self.tz.token.value
            elif requireName:
                raise self.tz.newSyntaxError("missing function identifier")

        x2 = self.static_context(self.tz, None, None, True, False, jsparser.NESTING_TOP)

        self.tz.mustMatch(LEFT_PAREN)
        if not self.tz.match(RIGHT_PAREN) :
            f.restArgument = False
            while True:
                tt = self.tz.get()
                if tt in (IDENTIFIER, SELF):
                    f.params.append(self.tz.token.value)
                    # rest argument must be the last in list
                    if f.restArgument: break
                    if self.tz.match(IDENTIFIER):
                        f.paramsType.append(self.tz.token.value)
                    else:
                        f.paramsType.append(None)
                    if self.tz.match(ASSIGN):
                        f.defaultParams.append(self.AssignExpression())
                    else:
                        f.defaultParams.append(None)
                elif tt == MUL:
                    self.tz.get()
                    f.restArgument = True
                else:
                    raise self.tz.newSyntaxError("missing formal parameter")

                if not self.tz.match(COMMA): break

            self.tz.mustMatch(RIGHT_PAREN)

        # Do we have an expression closure or a normal body?
        tt = self.tz.get()
        if (tt != LEFT_CURLY) :
            self.tz.unget()

        f.body = Script(self.tz, FUNCTION)

        if (tt == LEFT_CURLY) :
            self.tz.mustMatch(RIGHT_CURLY)

        f.end = self.tz.token.end
        f.functionForm = functionForm
        if (functionForm == jsparser.DECLARED_FORM) :
            self.parentScript.funDecls.append(f)
        return f

    def Identifier(self):
        n = Node(self.tz)
        if self.tz.match(AS):
            self.tz.mustMatch(IDENTIFIER)
            n.varType = self.tz.token.value
        else:
            n.varType = None
        return n

class Role(Node, StaticContext):
    """
    Role context
    """
    def __init__(self, parent, *args, **rest):
        StaticContext.__init__(self, parent, *args, **rest)


def parse(source, filename=None, starting_line_number=1):
    """Parse some Jooscript

    Args:
        source: the Jooscript source, as a string
        filename: the filename to include in messages
        starting_line_number: the line number of the first line of the
            passed in source, for output messages
    Returns:
        the parsed source code data structure
    Raises:
        ParseError
    """
    t = Tokenizer(source, filename, starting_line_number)
    n = Script(t)
    if not t.done:
        raise t.newSyntaxError("Syntax error")
    return n
